/*
* Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::{collections::BTreeSet, path::Path};

use anyhow::Context;
use convert_case::{Case, Casing};

use crate::msg_def::{BaseType, Field, FieldType, MsgDef, MsgDefMap};

impl MsgDefMap {
    pub async fn gen_arkts_protos<P: AsRef<Path>>(&self, out_dir: P) -> anyhow::Result<()> {
        let out_dir = out_dir.as_ref();
        tokio::fs::create_dir_all(out_dir).await
            .context(format!("failed to create '{}'", out_dir.display()))?;

        let builtin_files = [
            ("@ohos.robot.ohmw.BaseMessage.d.ts", include_str!("../../../interfaces/arkts/@ohos.robot.ohmw.BaseMessage.d.ts")),
            ("@ohos.robot.ohmw.MessageTime.d.ts", include_str!("../../../interfaces/arkts/@ohos.robot.ohmw.MessageTime.d.ts")),
            ("@ohos.robot.ohmw.MessageDuration.d.ts", include_str!("../../../interfaces/arkts/@ohos.robot.ohmw.MessageDuration.d.ts")),
            ("@ohos.robot.ohmw.Node.d.ts", include_str!("../../../interfaces/arkts/@ohos.robot.ohmw.Node.d.ts")),
            ("@ohos.robot.ohmw.MWServer.d.ts", include_str!("../../../interfaces/arkts/@ohos.robot.ohmw.MWServer.d.ts")),
        ];

        for (path, content) in builtin_files {
            let file_path = out_dir.join(path);
            tokio::fs::write(&file_path, content).await
                .context(format!("failed to write ArkTS builtin file to '{}'", file_path.display()))?;
        }

        for (pkg_name, msg_defs) in &self.map {
            let pkg_dir = out_dir.join(pkg_name);
            tokio::fs::create_dir_all(&pkg_dir).await
                .context(format!("failed to create '{}'", pkg_dir.display()))?;

            for (msg_name, msg_def) in msg_defs.iter() {
                let mut content = String::new();
                msg_def.gen_arkts_proto(pkg_name, msg_name, &mut content)
                    .context(format!("failed to generate ArkTS proto for '{}::{}'", pkg_name, msg_name))?;

                let msg_proto_path = pkg_dir.join(format!("{}.d.ts", msg_name.to_case(Case::Pascal)));
                tokio::fs::write(&msg_proto_path, content).await
                    .context(format!("failed to write ArkTS proto to '{}'", msg_proto_path.display()))?;
            }
        }

        let index_path = out_dir.join("index.d.ts");
        let mut content = String::new();
        self.gen_arkts_index(&mut content)
            .context("failed to generate ArkTS index")?;
        tokio::fs::write(&index_path, content).await
            .context(format!("failed to write ArkTS index to '{}'", index_path.display()))?;

        Ok(())
    }

    fn gen_arkts_index<W: std::fmt::Write>(&self, mut f: W) -> std::fmt::Result {
        writeln!(f, "export * from './@ohos.robot.ohmw.BaseMessage';")?;
        writeln!(f, "export * from './@ohos.robot.ohmw.MessageTime';")?;
        writeln!(f, "export * from './@ohos.robot.ohmw.MessageDuration';")?;
        writeln!(f, "export * from './@ohos.robot.ohmw.Node';")?;
        writeln!(f, "export * from './@ohos.robot.ohmw.MWServer';")?;

        for (pkg_name, msg_defs) in &self.map {
            for (msg_name, _) in msg_defs.iter() {    
                writeln!(f, "export * from './{}/{}';",
                    pkg_name, msg_name.to_case(Case::Pascal))?;
            }
        }

        Ok(())
    }
}

impl MsgDef {
    fn gen_arkts_proto<W: std::fmt::Write>(&self, pkg_name: &str, msg_name: &str, mut f: W) -> std::fmt::Result {
        self.gen_arkts_proto_imports(&mut f)?;
        writeln!(f)?;
        self.gen_arkts_class(pkg_name, msg_name, &mut f)?;
        Ok(())
    }

    fn gen_arkts_proto_imports<W: std::fmt::Write>(&self, mut f: W) -> std::fmt::Result {
        let mut imports = BTreeSet::from_iter([
            ("BaseMessage".to_string(), "../@ohos.robot.ohmw.BaseMessage".to_string()),
        ]);

        for field in &self.fields {
            let base_type = &field.field_type.base_type;
            match (base_type.pkg_name(), base_type.msg_name()) {
                (Some(pkg_name), msg_name) => imports.insert((
                    Self::arkts_class_name(pkg_name, msg_name),
                    format!("../{}/{}", pkg_name, msg_name.to_case(Case::Pascal)),
                )),
                (None, msg_name) => match msg_name {
                    "time" => imports.insert((
                        "MessageTime".to_string(),
                        "../@ohos.robot.ohmw.MessageTime".to_string(),
                    )),
                    "duration" => imports.insert((
                        "MessageDuration".to_string(),
                        "../@ohos.robot.ohmw.MessageDuration".to_string(),
                    )),
                    _ => false,
                },
            };
        }

        for (type_name, import_path) in imports {
            writeln!(f, "import {{ {} }} from '{}';", type_name, import_path)?;
        }

        Ok(())
    }

    fn gen_arkts_class<W: std::fmt::Write>(&self, pkg_name: &str, msg_name: &str, mut f: W) -> std::fmt::Result {
        writeln!(f, "export class {} extends BaseMessage {{", Self::arkts_class_name(pkg_name, msg_name))?;
        for field in &self.fields {
            writeln!(f, "    {}: {};", field.field_name_to_arkts(), field.field_type.to_arkts_type())?;
        }
        writeln!(f, "}}")?;
        Ok(())
    }

    pub fn arkts_class_name(pkg_name: &str, msg_name: &str) -> String {
        format!("Message_{}_{}", pkg_name, msg_name.to_case(Case::Pascal))
    }
}

impl Field {
    fn field_name_to_arkts(&self) -> String {
        let s = self.field_name.to_case(Case::Camel);
        match s.as_str() {
            "abstract" | "any" | "as" | "asserts" | "bigint" | "boolean" | "break" | "case" | "catch" |
            "class" | "const" | "constructor" | "continue" | "debugger" | "declare" | "default" | "delete" |
            "do" | "else" | "enum" | "export" | "extends" | "false" | "finally" | "for" | "from" |
            "function" | "if" | "implements" | "import" | "in" | "infer" | "instanceof" | "interface" |
            "is" | "keyof" | "let" | "module" | "namespace" | "never" | "new" | "null" | "number" |
            "object" | "package" | "private" | "protected" | "public" | "readonly" | "require" | "global" |
            "return" | "set" | "static" | "string" | "super" | "switch" | "symbol" | "this" | "throw" |
            "true" | "try" | "type" | "typeof" | "undefined" | "unique" | "unknown" | "var" | "void" |
            "while" | "with" | "yield" | "async" | "await" | "of" => {
                format!("_{}", s)
            },
            _ => s,
        }
    }
}

impl FieldType {
    fn to_arkts_type(&self) -> String {
        if self.is_array {
            format!("{}[]", self.base_type.to_arkts_type())
        } else {
            self.base_type.to_arkts_type()
        }
    }
}

impl BaseType {
    fn to_arkts_type(&self) -> String {
        match (self.pkg_name(), self.msg_name()) {
            // TODO: shorter type names
            (Some(pkg_name), msg_name) => MsgDef::arkts_class_name(pkg_name, msg_name),
            (None, msg_name) => match msg_name {
                "bool" => "boolean".to_string(),
                "byte" => "number".to_string(),
                "char" => "number".to_string(),
                "int8" => "number".to_string(),
                "uint8" => "number".to_string(),
                "int16" => "number".to_string(),
                "uint16" => "number".to_string(),
                "int32" => "number".to_string(),
                "uint32" => "number".to_string(),
                "int64" => "number".to_string(), // TODO: overflow
                "uint64" => "number".to_string(), // TODO: overflow
                "float32" => "number".to_string(),
                "float64" => "number".to_string(),
                "string" => "string".to_string(),
                "time" => "MessageTime".to_string(),
                "duration" => "MessageDuration".to_string(),
                other => format!("not_found_{}", other.to_case(Case::Pascal)),
            },
        }
    }
}
